using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Gko.Utils.Interfaces;

namespace Gko.Utils
{
	public static class BindableProperty
	{
		public static BindableProperty<TBindable, TProperty> Create<TBindable, TProperty>(string name) where TBindable : IBindable
		{
			return Create<TBindable, TProperty>(name, default(TProperty));
		}

		public static BindableProperty<TBindable, TProperty> Create<TBindable, TProperty>(string name, TProperty defaultValue) where TBindable : IBindable
		{
			return new BindableProperty<TBindable, TProperty>(name, defaultValue);
		}
	}

	public class BindableProperty<TBindable, TProperty> where TBindable : IBindable
	{
		private readonly string name;
		private readonly TProperty defaultValue;

		private readonly Dictionary<BindablePropertyDescription<TBindable, TProperty>, TProperty> propertyValues =
			new Dictionary<BindablePropertyDescription<TBindable, TProperty>, TProperty>();

		private readonly Dictionary<TBindable, BindablePropertyDescription<TBindable, TProperty>> descriptions =
			new Dictionary<TBindable, BindablePropertyDescription<TBindable, TProperty>>();

		private readonly List<PropertyBinding<TBindable, TProperty>> bindings =
			new List<PropertyBinding<TBindable, TProperty>>();

		private readonly List<Action<TBindable, TProperty, TProperty>> callbacks = new List<Action<TBindable, TProperty, TProperty>>();

		public BindableProperty(string name, TProperty defaultValue)
		{
			this.name = name;
			this.defaultValue = defaultValue;
		}

		public string Name
		{
			get
			{
				return this.name;
			}
		}

		public TProperty DefaultValue
		{
			get
			{
				return this.defaultValue;
			}
		}

		private static string GetPropertyName<TValue>(Expression<Func<TBindable, TValue>> expression)
		{
			var exprMember = expression.Body as MemberExpression;
			if (exprMember != null) {
				return exprMember.Member.Name;
			}

			throw new InvalidOperationException("GetPropertyName failed on " + expression);
		}

		public BindableProperty<TBindable, TProperty> Bind(Expression<Func<TBindable, TProperty>> expr)
		{
			return this.Bind(expr, v => v);
		}

		public BindableProperty<TBindable, TProperty> Bind(Expression<Func<TBindable, TProperty>> expr, Func<TProperty, TProperty> converter)
		{
			return this.Bind<TProperty>(expr, converter);
		}

		public BindableProperty<TBindable, TProperty> Bind<TOther>(Expression<Func<TBindable, TOther>> expr, Func<TProperty, TOther> converter)
		{
			this.BindCore(GetPropertyName(expr), converter);
			return this;
		}

		private void BindCore<TOther>(string propertyName, Func<TProperty, TOther> converter)
		{
			this.bindings.Add(new PropertyBinding<TBindable, TProperty>(this, propertyName, v => converter(v)));
		}

		public BindableProperty<TBindable, TProperty> Bind<TOtherBindable, TOther>(BindableProperty<TOtherBindable, TOther> property, Func<TProperty, TOther> converter) where TOtherBindable : IBindable
		{
			this.BindCore(property.Name, converter);
			return this;
		}

		public BindableProperty<TBindable, TProperty> Bind<TOther>(BindableProperty<TBindable, TOther> property, Func<TProperty, TOther> converter)
		{
			return this.Bind<TBindable, TOther>(property, converter);
		}

		public BindableProperty<TBindable, TProperty> Bind(BindableProperty<TBindable, TProperty> property)
		{
			return this.Bind<TProperty>(property, v => v);
		}

		public BindableProperty<TBindable, TProperty> OnChanged(Action<TBindable, TProperty, TProperty> callback)
		{
			this.callbacks.Add(callback);
			return this;
		}

		public BindableProperty<TBindable, TProperty> OnChanged(Action<TBindable, TProperty> callback)
		{
			return this.OnChanged((s, n, o) => callback(s, n));
		}

		public BindableProperty<TBindable, TProperty> OnChanged(TBindable bindable, Func<TBindable, Action<TProperty, TProperty>> callback)
		{
			return this.OnChanged(bindable, callback(bindable));
		}

		public BindableProperty<TBindable, TProperty> OnChanged(TBindable bindable, Func<TBindable, Action<TProperty>> callback)
		{
			return this.OnChanged(bindable, callback(bindable));
		}

		public BindableProperty<TBindable, TProperty> OnChanged(TBindable bindable, Action<TProperty, TProperty> callback)
		{
			return this.OnChanged(
				(b, n, o) => {
					if (ReferenceEquals(b, bindable)) {
						callback(n, o);
					}
				}
			);
		}

		public BindableProperty<TBindable, TProperty> OnChanged(TBindable bindable, Action<TProperty> callback)
		{
			return OnChanged(bindable, (n, o) => callback(n));
		}

		private BindablePropertyDescription<TBindable, TProperty> CreateDescription(TBindable bindable)
		{
			BindablePropertyDescription<TBindable, TProperty> description;
			if (this.descriptions.TryGetValue(bindable, out description)) {
				return description;
			}
			return this.descriptions[bindable] = new BindablePropertyDescription<TBindable, TProperty>(bindable, this.name);
		}

		public TProperty Get(TBindable bindable)
		{
			TProperty value;
			var description = this.CreateDescription(bindable);
			if (this.propertyValues.TryGetValue(description, out value)) {
				return value;
			}
			return this.propertyValues[description] = this.DefaultValue;
		}

		public void Set(TBindable bindable, TProperty value)
		{
			var description = this.CreateDescription(bindable);
			TProperty oldValue;
			if (this.propertyValues.TryGetValue(description, out oldValue)) {
				this.propertyValues[description] = value;
				this.OnPropertyChanged(bindable, oldValue, value);
			}

		}

		private void OnPropertyChanged(TBindable bindable, TProperty oldValue, TProperty value)
		{
			foreach (var binding in this.bindings) {
				binding.UpdateDestination(bindable, value);
			}
			bindable.OnPropertyChanged(this.name);
			foreach (var callback in this.callbacks) {
				callback(bindable, value, oldValue);
			}
		}

		public void UpdateDestination(TBindable bindable)
		{
			var value = this.Get(bindable);
			foreach (var binding in this.bindings) {
				binding.UpdateDestination(bindable, value);
			}
		}
	}
}